#include <cstdlib>
#include <vector>
#include "UTAXI4RAM.hpp"
#include "openvip/ovip.h"
#include "openvip_third/nutshell.h"

using namespace xspcomm;

XClock *pclk;
ovip::AXI4MasterPort *pram_bus;
UTAXI4RAM *pdut = nullptr;

inline uint64_t rand_data() { return rand() * 0x5a7f56ff8f6b2e91; }
inline uint64_t rand_addr() { return rand() % 512 * 8; }

uint64_t block_read(int addr) {
    uint64_t data;
    assert(pram_bus->send_read_address(addr));
    while(!pram_bus->receive_read_data(data)) pclk->Step();
    // Info("[addr: %d] Receive read data", addr);
    return data;
}

void block_write(int addr, uint64_t data) {
    uint8_t resp;
    assert(pram_bus->write(addr, data));
    while(!pram_bus->receive_write_response(resp)) pclk->Step();
    // Info("[addr: %d] Receive write response", addr);
    assert(resp == 0);
}

ovip::AXI4MasterPort::DataVec block_read_burst(int addr, int len) {
    ovip::AXI4MasterPort::DataVec data;
    assert(pram_bus->send_read_burst(addr, len));
    while(!pram_bus->receive_read_burst(data)) pclk->Step();
    // Info("[addr: %d] Receive read burst data", addr);
    return data;
}

void block_write_burst(int addr, const ovip::AXI4MasterPort::DataVec& data) {
    uint8_t resp;
    assert(pram_bus->write_burst(addr, data));
    while(!pram_bus->receive_write_response(resp)) pclk->Step();
    // Info("[addr: %d] Receive write burst responese", addr);
    assert(resp == 0);
}

uint64_t ram[512];
void test_axi4ram() {
    /* Init DUT */

    char **argv = new char *[1];
    argv[0]     = new char[50];
    strcpy(argv[0], "dut/UT_nutshell_axi4ram_trace/libDPIAXI4RAM.so");
    UTAXI4RAM dut(1, argv);
    pdut = &dut;

    std::function<int(bool)> stepfunc = [&dut](bool d) {
        dut.step(d);
        return 0;
    };
    XClock clk(stepfunc, {&dut.clock}, {&dut.port});
    ovip::AXI4MasterPort ram_bus("ram_bus", dut.port, clk, "io_in_");

    pclk = &clk;
    pram_bus = &ram_bus;

    dut.port["reset"] = 1;
    clk.Step();
    dut.port["reset"] = 0;
    for (int i = 0; i < 5; i++)
        clk.Step();

    /* Test */

    // Test read and write
    for (int addr = 0; addr < 4096; addr += 8) {
        uint64_t expect_data = ram[addr / 8] = rand_data();
        block_write(addr, expect_data);
        uint64_t actual_data = block_read(addr);
        assert(actual_data == expect_data);
    }
    for (int i = 0; i < 1024; i++) {
        if (rand() % 2) {
            int addr = rand_addr();
            ram[addr / 8] = rand_data();
            block_write(addr, ram[addr / 8]);
        } else {
            int addr = rand_addr();
            assert(block_read(addr) == ram[addr / 8]);
        }
    }

    // Test read burst
    for (int i = 0; i < 1024; i++) {
        int len = rand() % 128 + 1;
        int addr = rand() % (512 - len) * 8;
        ovip::AXI4MasterPort::DataVec data = block_read_burst(addr, len);
        for (int j = 0; j < len; j++)
            assert(data[j] == ram[addr / 8 + j]);
    }

    // Test write burst
    for (int i = 0; i < 1024; i++) {
        int len = rand() % 128 + 1;
        int addr = rand() % (512 - len) * 8;
        ovip::AXI4MasterPort::DataVec data;
        for (int i = 0; i < len; i++) {
            ram[addr / 8 + i] = rand_data();
            data.push_back(ram[addr / 8 + i]);
        }
        block_write_burst(addr, data);
    }
    for (int addr = 0; addr < 4096; addr += 8) {
        uint64_t expect_data = ram[addr / 8];
        uint64_t actual_data = block_read(addr);
        assert(actual_data == expect_data);
    }



    for (int i = 0; i < 10; i++)
        pclk->Step();

    Info("TEST PASSED");
}


int main() {
    Info("Test axi4ram ...");

    signal(SIGINT, [](int sig) {
        pdut->finalize();
        Info("SIGINT(%d) received, exit!", sig);
        exit(0);
    });

    test_axi4ram();

    return 0;
}
